package com.topideal.supplychain.hades.util;

/*
 * #%L ch-commons-util %% Copyright (C) 2012 Cloudhopper by Twitter %% Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License. #L%
 */

/**
 * Utility class for encoding or decoding objects to a hexidecimal format.
 * 
 * @author joelauer (twitter: @jjlauer or
 *         <a href="http://twitter.com/jjlauer" target=window>http://twitter.com/jjlauer</a>)
 */
public class HexUtil {

    public static char[] HEX_TABLE =
            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    /**
     * Creates a String from a byte array with each byte in a "Big Endian" hexidecimal format. For
     * example, a byte 0x34 will return a String "34". A byte array of { 0x34, 035 } would return
     * "3435".
     * 
     * @param buffer The StringBuilder the byte array in hexidecimal format will be appended to. If
     *        the buffer is null, this method will throw a NullPointerException.
     * @param bytes The byte array that will be converted to a hexidecimal String. If the byte array
     *        is null, this method will append nothing (a noop)
     */
    public static String toHexString(byte[] bytes) {
        if (bytes == null) {
            return "";
        }
        return toHexString(bytes, 0, bytes.length);
    }

    /**
     * Creates a String from a byte array with each byte in a "Big Endian" hexidecimal format. For
     * example, a byte 0x34 will return a String "34". A byte array of { 0x34, 035 } would return
     * "3435".
     * 
     * @param buffer The StringBuilder the byte array in hexidecimal format will be appended to. If
     *        the buffer is null, this method will throw a NullPointerException.
     * @param bytes The byte array that will be converted to a hexidecimal String. If the byte array
     *        is null, this method will append nothing (a noop)
     * @param offset The offset in the byte array to start from. If the offset or length combination
     *        is invalid, this method will throw an IllegalArgumentException.
     * @param length The length (from the offset) to conver the bytes. If the offset or length
     *        combination is invalid, this method will throw an IllegalArgumentException.
     */
    public static String toHexString(byte[] bytes, int offset, int length) {
        if (bytes == null) {
            return "";
        }
        assertOffsetLengthValid(offset, length, bytes.length);
        // each byte is 2 chars in string
        StringBuilder buffer = new StringBuilder(length * 2);
        appendHexString(buffer, bytes, offset, length);
        return buffer.toString();
    }

    /**
     * Appends a byte array to a StringBuilder with each byte in a "Big Endian" hexidecimal format.
     * For example, a byte 0x34 will be appended as a String in format "34". A byte array of { 0x34,
     * 035 } would append "3435".
     * 
     * @param buffer The StringBuilder the byte array in hexidecimal format will be appended to. If
     *        the buffer is null, this method will throw a NullPointerException.
     * @param bytes The byte array that will be converted to a hexidecimal String. If the byte array
     *        is null, this method will append nothing (a noop)
     */
    public static void appendHexString(StringBuilder buffer, byte[] bytes) {
        assertNotNull(buffer);
        if (bytes == null) {
            return; // do nothing (a noop)
        }
        appendHexString(buffer, bytes, 0, bytes.length);
    }

    /**
     * Appends a byte array to a StringBuilder with each byte in a "Big Endian" hexidecimal format.
     * For example, a byte 0x34 will be appended as a String in format "34". A byte array of { 0x34,
     * 035 } would append "3435".
     * 
     * @param buffer The StringBuilder the byte array in hexidecimal format will be appended to. If
     *        the buffer is null, this method will throw a NullPointerException.
     * @param bytes The byte array that will be converted to a hexidecimal String. If the byte array
     *        is null, this method will append nothing (a noop)
     * @param offset The offset in the byte array to start from. If the offset or length combination
     *        is invalid, this method will throw an IllegalArgumentException.
     * @param length The length (from the offset) to conver the bytes. If the offset or length
     *        combination is invalid, this method will throw an IllegalArgumentException.
     */
    static public void appendHexString(StringBuilder buffer, byte[] bytes, int offset, int length) {
        assertNotNull(buffer);
        if (bytes == null) {
            return; // do nothing (a noop)
        }
        assertOffsetLengthValid(offset, length, bytes.length);
        int end = offset + length;
        for (int i = offset; i < end; i++) {
            int nibble1 = (bytes[i] & 0xF0) >>> 4;
            int nibble0 = (bytes[i] & 0x0F);
            buffer.append(HEX_TABLE[nibble1]);
            buffer.append(HEX_TABLE[nibble0]);
        }
    }

    /**
     * Creates a 2 character hex String from a byte with the byte in a "Big Endian" hexidecimal
     * format. For example, a byte 0x34 will be returned as a String in format "34". A byte of value
     * 0 will be returned as "00".
     * 
     * @param value The byte value that will be converted to a hexidecimal String.
     */
    static public String toHexString(byte value) {
        StringBuilder buffer = new StringBuilder(2);
        appendHexString(buffer, value);
        return buffer.toString();
    }

    /**
     * Appends 2 characters to a StringBuilder with the byte in a "Big Endian" hexidecimal format.
     * For example, a byte 0x34 will be appended as a String in format "34". A byte of value 0 will
     * be appended as "00".
     * 
     * @param buffer The StringBuilder the byte value in hexidecimal format will be appended to. If
     *        the buffer is null, this method will throw a NullPointerException.
     * @param value The byte value that will be converted to a hexidecimal String.
     */
    static public void appendHexString(StringBuilder buffer, byte value) {
        assertNotNull(buffer);
        int nibble = (value & 0xF0) >>> 4;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x0F);
        buffer.append(HEX_TABLE[nibble]);
    }

    /**
     * Creates a 4 character hex String from a short with the short in a "Big Endian" hexidecimal
     * format. For example, a short 0x1234 will be returned as a String in format "1234". A short of
     * value 0 will be returned as "0000".
     * 
     * @param value The short value that will be converted to a hexidecimal String.
     */
    static public String toHexString(short value) {
        StringBuilder buffer = new StringBuilder(4);
        appendHexString(buffer, value);
        return buffer.toString();
    }

    /**
     * Appends 4 characters to a StringBuilder with the short in a "Big Endian" hexidecimal format.
     * For example, a short 0x1234 will be appended as a String in format "1234". A short of value 0
     * will be appended as "0000".
     * 
     * @param buffer The StringBuilder the short value in hexidecimal format will be appended to. If
     *        the buffer is null, this method will throw a NullPointerException.
     * @param value The short value that will be converted to a hexidecimal String.
     */
    static public void appendHexString(StringBuilder buffer, short value) {
        assertNotNull(buffer);
        int nibble = (value & 0xF000) >>> 12;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x0F00) >>> 8;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x00F0) >>> 4;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x000F);
        buffer.append(HEX_TABLE[nibble]);
    }

    /**
     * Creates an 8 character hex String from an int twith the int in a "Big Endian" hexidecimal
     * format. For example, an int 0xFFAA1234 will be returned as a String in format "FFAA1234". A
     * int of value 0 will be returned as "00000000".
     * 
     * @param value The int value that will be converted to a hexidecimal String.
     */
    static public String toHexString(int value) {
        StringBuilder buffer = new StringBuilder(8);
        appendHexString(buffer, value);
        return buffer.toString();
    }

    /**
     * Appends 8 characters to a StringBuilder with the int in a "Big Endian" hexidecimal format.
     * For example, a int 0xFFAA1234 will be appended as a String in format "FFAA1234". A int of
     * value 0 will be appended as "00000000".
     * 
     * @param buffer The StringBuilder the int value in hexidecimal format will be appended to. If
     *        the buffer is null, this method will throw a NullPointerException.
     * @param value The int value that will be converted to a hexidecimal String.
     */
    static public void appendHexString(StringBuilder buffer, int value) {
        assertNotNull(buffer);
        int nibble = (value & 0xF0000000) >>> 28;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x0F000000) >>> 24;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x00F00000) >>> 20;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x000F0000) >>> 16;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x0000F000) >>> 12;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x00000F00) >>> 8;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x000000F0) >>> 4;
        buffer.append(HEX_TABLE[nibble]);
        nibble = (value & 0x0000000F);
        buffer.append(HEX_TABLE[nibble]);
    }

    /**
     * Creates a 16 character hex String from a long with the long in a "Big Endian" hexidecimal
     * format. For example, a long 0xAABBCCDDEE123456 will be returned as a String in format
     * "AABBCCDDEE123456". A long of value 0 will be returned as "0000000000000000".
     * 
     * @param value The long value that will be converted to a hexidecimal String.
     */
    static public String toHexString(long value) {
        StringBuilder buffer = new StringBuilder(16);
        appendHexString(buffer, value);
        return buffer.toString();
    }

    /**
     * Appends 16 characters to a StringBuilder with the long in a "Big Endian" hexidecimal format.
     * For example, a long 0xAABBCCDDEE123456 will be appended as a String in format
     * "AABBCCDDEE123456". A long of value 0 will be appended as "0000000000000000".
     * 
     * @param buffer The StringBuilder the long value in hexidecimal format will be appended to. If
     *        the buffer is null, this method will throw a NullPointerException.
     * @param value The long value that will be converted to a hexidecimal String.
     */
    static public void appendHexString(StringBuilder buffer, long value) {
        appendHexString(buffer, (int) ((value & 0xFFFFFFFF00000000L) >>> 32));
        appendHexString(buffer, (int) (value & 0x00000000FFFFFFFFL));
    }

    static private void assertNotNull(StringBuilder buffer) {
        if (buffer == null) {
            throw new NullPointerException("The buffer cannot be null");
        }
    }

    static private void assertOffsetLengthValid(int offset, int length, int arrayLength) {
        if (offset < 0) {
            throw new IllegalArgumentException("The array offset was negative");
        }
        if (length < 0) {
            throw new IllegalArgumentException("The array length was negative");
        }
        if (offset + length > arrayLength) {
            throw new ArrayIndexOutOfBoundsException(
                    "The array offset+length would access past end of array");
        }
    }

    /**
     * Converts a hexidecimal character such as '0' or 'A' or 'a' to its integer value such as 0 or
     * 10. Used to decode hexidecimal Strings to integer values.
     * 
     * @param c The hexidecimal character
     * @return The integer value the character represents
     * @throws IllegalArgumentException Thrown if a character that does not represent a hexidecimal
     *         character is used.
     */
    static public int hexCharToIntValue(char c) {
        if (c == '0') {
            return 0;
        } else if (c == '1') {
            return 1;
        } else if (c == '2') {
            return 2;
        } else if (c == '3') {
            return 3;
        } else if (c == '4') {
            return 4;
        } else if (c == '5') {
            return 5;
        } else if (c == '6') {
            return 6;
        } else if (c == '7') {
            return 7;
        } else if (c == '8') {
            return 8;
        } else if (c == '9') {
            return 9;
        } else if (c == 'A' || c == 'a') {
            return 10;
        } else if (c == 'B' || c == 'b') {
            return 11;
        } else if (c == 'C' || c == 'c') {
            return 12;
        } else if (c == 'D' || c == 'd') {
            return 13;
        } else if (c == 'E' || c == 'e') {
            return 14;
        } else if (c == 'F' || c == 'f') {
            return 15;
        } else {
            throw new IllegalArgumentException(
                    "The character [" + c + "] does not represent a valid hex digit");
        }
    }

    /**
     * Creates a byte array from a CharSequence (String, StringBuilder, etc.) containing only valid
     * hexidecimal formatted characters. Each grouping of 2 characters represent a byte in "Big
     * Endian" format. The hex CharSequence must be an even length of characters. For example, a
     * String of "1234" would return the byte array { 0x12, 0x34 }.
     * 
     * @param hexString The String, StringBuilder, etc. that contains the sequence of hexidecimal
     *        character values.
     * @return A new byte array representing the sequence of bytes created from the sequence of
     *         hexidecimal characters. If the hexString is null, then this method will return null.
     */
    public static byte[] toByteArray(CharSequence hexString) {
        if (hexString == null) {
            return null;
        }
        return toByteArray(hexString, 0, hexString.length());
    }

    /**
     * Creates a byte array from a CharSequence (String, StringBuilder, etc.) containing only valid
     * hexidecimal formatted characters. Each grouping of 2 characters represent a byte in "Big
     * Endian" format. The hex CharSequence must be an even length of characters. For example, a
     * String of "1234" would return the byte array { 0x12, 0x34 }.
     * 
     * @param hexString The String, StringBuilder, etc. that contains the sequence of hexidecimal
     *        character values.
     * @param offset The offset within the sequence to start from. If the offset is invalid, will
     *        cause an IllegalArgumentException.
     * @param length The length from the offset to convert. If the length is invalid, will cause an
     *        IllegalArgumentException.
     * @return A new byte array representing the sequence of bytes created from the sequence of
     *         hexidecimal characters. If the hexString is null, then this method will return null.
     */
    public static byte[] toByteArray(CharSequence hexString, int offset, int length) {
        if (hexString == null) {
            return null;
        }
        assertOffsetLengthValid(offset, length, hexString.length());

        // a hex string must be in increments of 2
        if ((length % 2) != 0) {
            throw new IllegalArgumentException(
                    "The hex string did not contain an even number of characters [actual=" + length
                            + "]");
        }

        // convert hex string to byte array
        byte[] bytes = new byte[length / 2];

        int j = 0;
        int end = offset + length;

        for (int i = offset; i < end; i += 2) {
            int highNibble = hexCharToIntValue(hexString.charAt(i));
            int lowNibble = hexCharToIntValue(hexString.charAt(i + 1));
            bytes[j++] = (byte) (((highNibble << 4) & 0xF0) | (lowNibble & 0x0F));
        }
        return bytes;
    }
}

